/*
 *   SimpleCalculationModel - A simple implementaion of Calculation Model
 *   Copyright (C) 2012 Joe Huang
 *   
 * 	 This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */


package calculator.model;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Observable;
import java.util.Stack;

public class SimpleCalculationModel extends CalculationModel {

	final static boolean debugMode = false;
	
	/* This main() is only for unit test */
	public static void main(String[] args) throws IOException {
		
		// test print the CharTable
		Iterator it = CharTable.priTable.keySet().iterator();
		while (it.hasNext()) {
			System.out.println(it.next()+" ");
		}
		it = CharTable.priTable.values().iterator();
		while (it.hasNext()) {
			System.out.println(it.next()+" ");
		}
		it = CharTable.assoTable.keySet().iterator();
		while (it.hasNext()) {
			System.out.println(it.next()+" ");
		}
		it = CharTable.assoTable.values().iterator();
		while (it.hasNext()) {
			System.out.println(it.next()+" ");
		}
		
		BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
		String line;
		
		SimpleCalculationModel calModel = new SimpleCalculationModel();
		
		System.out.println("Input string:");
		while ((line = bf.readLine())!=null) {
			calModel.doCalculation(line);
			calModel.getAnswer();
			System.out.println("Input string:");
		}
		
	}
	
	
	public String getInputs() {
		return inputs;
	} // for any view(eg. GUI) to retrieve current inputs

	public String getAnswer() {
		return answer;
	} // for any view(eg. GUI) to retrieve current ans

	@Override
	public void doOperand(String btnName, String newInputs) {
		this.inputs = newInputs + btnName;
		this.setChanged();
	}

	@Override
	public void doOperator(String btnName, String newInputs) {
		this.inputs = newInputs + btnName;
		this.setChanged();
	}

	@Override
	public void doCalculation(String newInputs) {
		// Transform infix to postfix
		this.inputs = newInputs;
		String postfixExpr = infixToPostfix(newInputs);
		System.out.println("postfixExpr:" + postfixExpr);
		
		// Get the answer
		MutableDouble ans = new MutableDouble(0);
		if (computePostfix(postfixExpr, ans)) {
			System.out.println("Ans:" + ans.getValue());
			this.answer = "" + ans.getValue();
		}
		else {
			System.out.println("Computation Error!!!");
			this.answer = "Computation Error!!!";
		}
		this.setChanged();
	}

	@Override
	public void doBackSpace(String newInputs) {
		String str = newInputs;
		StringBuilder sb = new StringBuilder(str);
		if (str.length() > 0)
			this.inputs = sb.substring(0, str.length() - 1);
		this.setChanged();
	}

	@Override
	public void doClear() {
		this.inputs = "";
		this.answer = " ";
		this.setChanged();
	}
	
	
	private String infixToPostfix(String infixExpr) {
		StringBuilder postfixBuilder = new StringBuilder();
		
		Stack<Character> stk;
		if (debugMode) stk = new DebugStack<Character>();
		else stk = new Stack<Character>();
		
		char c;    // character outside the stack
		char in_c; // character in the stack
		int rtn=0;
		for (int i=0; i<infixExpr.length(); i++) {
			
			c = infixExpr.charAt(i);

			if (CharTable.isOperand(c))  {
				postfixBuilder.append(c);
			}
			else if (!CharTable.isOperator(c)) return null;
			else if (CharTable.isOperator(c)) {
				
				postfixBuilder.append(" ");
				
				if (!stk.empty()) {
					in_c = stk.peek();
					rtn = CharTable.comparePriority(c, in_c);
				}
				if (stk.empty() && c != ')') stk.push(c);
				else if (c == ')') {
					while (!stk.empty() && stk.peek()!='(') {
						postfixBuilder.append(stk.pop()+" ");
					}
					if (!stk.empty()) stk.pop(); // pop out '('
				}
				else if (rtn==1) stk.push(c); // char outside the stack has higher priority
				else if (rtn==-1) {
					while (!stk.empty()) {
						postfixBuilder.append(stk.pop()+" "); // char inside the stack has higher priority
						if (stk.empty()) break;
						rtn = CharTable.comparePriority(c, stk.peek());
						if (rtn == 1) break;
						else if (rtn == -1) continue;
						else return null;
					}
					stk.push(c);
				}
				else if (rtn==0){
					System.out.println("[ERROR] CharTable.comparePriority(char, char), check if character is defined in CharTable");
					return null;
				} else {
					System.out.println("[ERROR] Unexpected Error for CharTable.comparePriority(char, char)");
					return null;
				}
				
			} // end isOperator block
			
		}
		postfixBuilder.append(" ");
		while (!stk.empty()) {
			if (stk.peek()=='(') stk.pop();
			else { 
				postfixBuilder.append(stk.pop());
				postfixBuilder.append(" ");
			}
		}
		
		return postfixBuilder.toString();
	}
	
	
	private boolean computePostfix(String postfixExpr, MutableDouble ans) {
		 
		 if (postfixExpr==null) return false;
		 
		 Stack<Double> stk;
		 if (debugMode) stk = new DebugStack<Double>();
		 else stk = new Stack<Double>();
		 
		 StringBuilder tmpStr = new StringBuilder();
		 
		 double lhs;
		 double rhs;
		 
		 for (int i=0; i<postfixExpr.length(); i++) {
			 if (postfixExpr.charAt(i)==32 && tmpStr.toString().equals("")) continue;
			 else if (postfixExpr.charAt(i)==32) {
				try {
					stk.push(Double.parseDouble(tmpStr.toString()));
					tmpStr.setLength(0);
				} catch (Exception e) {
					return false;
				}
			 } else if (postfixExpr.charAt(i)!=32 && CharTable.isOperand(postfixExpr.charAt(i))) {
				 tmpStr.append(postfixExpr.charAt(i));
			 } else if (CharTable.isOperator(postfixExpr.charAt(i))) {
				 try {
					rhs = stk.pop();
					if (stk.empty() && postfixExpr.charAt(i)=='-') lhs=0; 
					else lhs = stk.pop();
					switch(postfixExpr.charAt(i)) {
						case '+':
							if (debugMode) System.out.println("Performing "+lhs+"+"+rhs);
							stk.push(lhs+rhs);
							break;
						case '-':
							if (debugMode) System.out.println("Performing "+lhs+"-"+rhs);
							stk.push(lhs-rhs);
							break;
						case '*':
							if (debugMode) System.out.println("Performing "+lhs+"*"+rhs);
							stk.push(lhs*rhs);
							break;
						case '/':
							if (debugMode) System.out.println("Performing "+lhs+"/"+rhs);
							stk.push(lhs/rhs);
							break;
						case '%':
							if (debugMode) System.out.println("Performing "+lhs+"%"+rhs);
							stk.push(lhs%rhs);
							break;
						case '^':
							if (debugMode) System.out.println("Performing "+lhs+"^"+rhs);
							stk.push(Math.pow(lhs, rhs));
							break;
						default:
							return false;
					}
					
				 } catch(Exception e) {
					 return false;
				 }
			 } else return false;
		 }
		 
		 try {
			 ans.setValue(stk.pop());
			 if (debugMode) System.out.println("computePostfix():" + ans.getValue());
		 } catch (Exception e) {
			 return false;
		 }
		 
		return true;
	 }
		
	
	
	

	private static String inputs;
	private static String answer;

}

class MutableDouble {
	private double value;

	public MutableDouble(double value) {
		this.value = value;
	}

	public double getValue() {
		return this.value;
	}

	public void setValue(double value) {
		this.value = value;
	}
}


class DebugStack<E> extends Stack<E> {
	@Override
	public E pop() {
		if (!empty()) System.out.println(peek()+" poped");
		return super.pop();
	}
	@Override
	public E push(E c) {
		System.out.println(c+" pushed");
		return super.push(c);
	}
	
}


//CalTable provides necessary methods to parse the string when doing infixToPostFix()
class CharTable {
	
	static final HashMap<Character, Integer> priTable = new HashMap<Character, Integer>();  // Priority Table;
	static final HashMap<Character, Integer> assoTable  = new HashMap<Character, Integer>(); // Association Table
	static {
		// Construct the priority table for operators
		priTable.put(')', 1);
		priTable.put('+', 2);
		priTable.put('-', 2);
		priTable.put('*', 3);
		priTable.put('/', 3);
		priTable.put('%', 3);
		priTable.put('^', 4);
		priTable.put('(', 5);
		// Construct the association table; 1 means left associative, 2 means right associative
		assoTable.put('+', 1);
		assoTable.put('-', 1);
		assoTable.put('*', 1);
		assoTable.put('/', 1);
		assoTable.put('%', 1);
		assoTable.put('^', 2);
	}
	
	/* out is char outside stack, in is char inside stack */
	static int comparePriority(char out, char in) { // return 1 if(out>in), -1 if(out<in), 0 if error occured
		//System.out.println("out:"+out+"in:"+in);
		if (!priTable.containsKey(out) || !priTable.containsKey(in)) return 0;
		else if (in == '(') return 1;
		else if (priTable.get(out) > priTable.get(in)) return 1;
		else if (priTable.get(out) < priTable.get(in)) return -1;
		else {
			if (!assoTable.containsKey(out) || !assoTable.containsKey(in)) return 0;
			else if (assoTable.get(out) == 1) return -1;
			else if (assoTable.get(out) == 2) return 1;
			else return 0;
		}
	}
	
	static boolean isOperand(char c) {
		if ( c==32 || (c >= '0' && c<='9') || c == '.') return true;
		else return false;
	}
	
	static boolean isOperator(char c) {
		if (priTable.containsKey(c)) return true;
		return false;
	}
	
}


